//@author A0099223B

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;

public class DateTimeParser extends Parser {

	private static final String FORMAT_12H_SPACE_AMPM = "hh a";
	private static final String FORMAT_12H_MIN_SPACE_AMPM = "hh:mm a";
	private static final String FORMAT_12H_MIN_AMPM = "hh:mma";
	private static final String FORMAT_12H_AMPM = "hha";
	private static final String FORMAT_24H_MIN = "HH:mm";
	private static final String FORMAT_DAY_MONTH = "dd/MM";
	private static final String FORMAT_DAY_MONTH_YEAR = "dd/MM/yy";
	private static final String FORMAT_DAY_OF_WEEK_SHORT = "EE";

	private static final String WORD_NEXT = "next";
	private static final String WORD_NXT = "nxt";
	private static final String WORD_TODAY = "today";
	private static final String WORD_TOMORROW = "tomorrow";
	private static final String WORD_MONDAY = "monday";
	private static final String WORD_MON = "mon";
	private static final String WORD_TUE = "tue";
	private static final String WORD_TUESDAY = "tuesday";
	private static final String WORD_WED = "wed";
	private static final String WORD_WEDNESDAY = "wednesday";
	private static final String WORD_THU = "thu";
	private static final String WORD_THURSDAY = "thursday";
	private static final String WORD_FRI = "fri";
	private static final String WORD_FRIDAY = "friday";
	private static final String WORD_SAT = "sat";
	private static final String WORD_SATURDAY = "saturday";
	private static final String WORD_SUN = "sun";
	private static final String WORD_SUNDAY = "sunday";

	private static final int INTEGER_NOT_DAY_OF_THE_WEEK = -1;
	private static final int INTEGER_FOR_MON = 2;
	private static final int INTEGER_FOR_TUE = 3;
	private static final int INTEGER_FOR_WED = 4;
	private static final int INTEGER_FOR_THU = 5;
	private static final int INTEGER_FOR_FRI = 6;
	private static final int INTEGER_FOR_SAT = 7;
	private static final int INTEGER_FOR_SUN = 1;
	private static final int INTEGER_DAYS_IN_A_WEEK = 7;
	private static final int INTEGER_ONE_DAY = 1;

	private static final String MESSAGE_WRONG_TIME_FORMAT = "\"%1$s\" is not an understood time format";
	protected static final boolean BOOLEAN_START_TIME = true;
	protected static final boolean BOOLEAN_END_TIME = false;

	private static final int ARRAY_INDEX_FIFTH_WORD = 4;
	private static final int ARRAY_INDEX_FOURTH_WORD = 3;
	private static final int ARRAY_INDEX_THIRD_WORD = 2;
	private static final int ARRAY_INDEX_SECOND_WORD = 1;
	private static final int ARRAY_INDEX_FIRST_WORD = 0;

	private static final int LIST_SIZE_FOUR_WORDS = 4;
	private static final int LIST_SIZE_THREE_WORDS = 3;
	private static final int LIST_SIZE_TWO_WORDS = 2;
	private static final int LIST_SIZE_ONE_WORD = 1;

	private static final int DEFAULT_START_TIME_HOUR = 0;
	private static final int DEFAULT_START_TIME_MINUTE = 0;

	private static final int DEFAULT_END_TIME_HOUR = 23;
	private static final int DEFAULT_END_TIME_MINUTE = 59;

	private static final int DEFAULT_TIME_MILISECOND = 0;
	private static final int DEFAULT_TIME_SECOND = 0;

	private static Date NO_RESULT = null;

	protected static Date parse(String userInput, boolean type)
			throws InvalidInputException {
		assert (userInput != null);

		ArrayList<String> wordArrayList = getWordsWithoutSpaces(userInput);

		Date dateTime = NO_RESULT;

		if (wordArrayList.size() == LIST_SIZE_ONE_WORD) {
			dateTime = processAsDateOrTime(wordArrayList, type);
		} else if (wordArrayList.size() == LIST_SIZE_TWO_WORDS) {
			dateTime = processTwoWords(wordArrayList, type);
		} else if (wordArrayList.size() == LIST_SIZE_THREE_WORDS) {
			dateTime = processThreeWords(wordArrayList, type);
		} else if (wordArrayList.size() == LIST_SIZE_FOUR_WORDS) {
			dateTime = processFourWords(wordArrayList, type);
		}

		if (dateTime == NO_RESULT) {
			throw new InvalidInputException(String.format(
					MESSAGE_WRONG_TIME_FORMAT, userInput));
		}

		return dateTime;
	}

	private static ArrayList<String> getWordsWithoutSpaces(String userInput) {
		String[] stringArray = userInput.split(STRING_WHITESPACE);
		ArrayList<String> wordList = new ArrayList<String>();
		for (String string : stringArray) {
			if (!string.equals("")) {
				wordList.add(string);
			}
		}

		return wordList;
	}

	private static Date processAsDateOrTime(ArrayList<String> wordArrayList,
			boolean type) {
		assert (wordArrayList.size() == LIST_SIZE_ONE_WORD);

		Date dateTime = processAsDate(wordArrayList.get(0), type);
		if (dateTime == NO_RESULT) {
			dateTime = processAsTime(wordArrayList.get(0));
		}
		return dateTime;

	}

	private static Date processTwoWords(ArrayList<String> wordArrayList,
			boolean type) {
		assert (wordArrayList.size() == LIST_SIZE_TWO_WORDS);

		Date dateTime = processAsTwoWordDate(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		dateTime = processAsTwoWordTime(wordArrayList);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		dateTime = processAsOneWordDateAndOneWordTime(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}

		dateTime = processAsOneWordTimeAndOneWordDate(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		return NO_RESULT;
	}

	private static Date processAsOneWordTimeAndOneWordDate(
			ArrayList<String> wordArrayList, boolean type) {
		if (wordArrayList.size() != LIST_SIZE_TWO_WORDS) {
			return NO_RESULT;
		}

		Date date = processAsDate(wordArrayList.get(ARRAY_INDEX_SECOND_WORD),
				type);
		Date time = processAsTime(wordArrayList.get(ARRAY_INDEX_FIRST_WORD));

		if (time != NO_RESULT && date != NO_RESULT) {
			return combineDateAndTime(date, time);
		} else {
			return NO_RESULT;
		}
	}

	private static Date processAsOneWordDateAndOneWordTime(
			ArrayList<String> wordArrayList, boolean type) {
		if (wordArrayList.size() != LIST_SIZE_TWO_WORDS) {
			return NO_RESULT;
		}

		Date date = processAsDate(wordArrayList.get(ARRAY_INDEX_FIRST_WORD),
				type);
		Date time = processAsTime(wordArrayList.get(ARRAY_INDEX_SECOND_WORD));

		if (time != NO_RESULT && date != NO_RESULT) {
			return combineDateAndTime(date, time);
		} else {
			return NO_RESULT;
		}

	}

	private static Date processThreeWords(ArrayList<String> wordArrayList,
			boolean type) {
		assert (wordArrayList.size() == LIST_SIZE_THREE_WORDS);

		Date dateTime = processAsTwoWordDateAndOneWordTime(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		dateTime = processAsOneWordDateAndTwoWordTime(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		dateTime = processAsTwoWordTimeAndOneWordDate(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		dateTime = processAsOneWordTimeAndTwoWordDate(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}

		return NO_RESULT;
	}

	private static Date processAsTwoWordDateAndOneWordTime(
			ArrayList<String> wordArrayList, boolean type) {

		ArrayList<String> dateWords = getFirstTwoWords(wordArrayList);
		Date date = processAsTwoWordDate(dateWords, type);
		Date time = processAsTime(wordArrayList.get(ARRAY_INDEX_THIRD_WORD));

		if (date != NO_RESULT && time != NO_RESULT) {
			return combineDateAndTime(date, time);
		}
		return NO_RESULT;
	}

	private static Date processAsOneWordDateAndTwoWordTime(
			ArrayList<String> wordArrayList, boolean type) {

		ArrayList<String> timeWords = getSecondAndThirdWord(wordArrayList);
		Date date = processAsDate(wordArrayList.get(ARRAY_INDEX_FIRST_WORD),
				type);
		Date time = processAsTwoWordTime(timeWords);

		if (date != NO_RESULT && time != NO_RESULT) {
			return combineDateAndTime(date, time);
		}
		return NO_RESULT;
	}

	private static Date processAsTwoWordTimeAndOneWordDate(
			ArrayList<String> wordArrayList, boolean type) {

		ArrayList<String> timeWords = getFirstTwoWords(wordArrayList);
		Date date = processAsDate(wordArrayList.get(ARRAY_INDEX_THIRD_WORD),
				type);
		Date time = processAsTwoWordTime(timeWords);

		if (date != NO_RESULT && time != NO_RESULT) {
			return combineDateAndTime(date, time);
		}
		return NO_RESULT;
	}

	private static Date processAsOneWordTimeAndTwoWordDate(
			ArrayList<String> wordArrayList, boolean type) {

		ArrayList<String> dateWords = getSecondAndThirdWord(wordArrayList);
		Date date = processAsTwoWordDate(dateWords, type);
		Date time = processAsTime(wordArrayList.get(ARRAY_INDEX_FIRST_WORD));

		if (date != NO_RESULT && time != NO_RESULT) {
			return combineDateAndTime(date, time);
		}
		return NO_RESULT;
	}

	private static Date processFourWords(ArrayList<String> wordArrayList,
			boolean type) {
		assert (wordArrayList.size() == 4);

		Date dateTime = processTwoWordDateAndTwoWordTime(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}
		dateTime = processTwoWordTimeAndTwoWordDate(wordArrayList, type);
		if (dateTime != NO_RESULT) {
			return dateTime;
		}

		return NO_RESULT;
	}

	private static Date processTwoWordTimeAndTwoWordDate(
			ArrayList<String> wordArrayList, boolean type) {

		ArrayList<String> timeWords = getFirstTwoWords(wordArrayList);
		ArrayList<String> dateWords = getThirdAndFourthWords(wordArrayList);
		Date date = processAsTwoWordDate(dateWords, type);
		Date time = processAsTwoWordTime(timeWords);

		if (date != NO_RESULT && time != NO_RESULT) {
			return combineDateAndTime(date, time);
		}
		return NO_RESULT;
	}

	private static Date processTwoWordDateAndTwoWordTime(
			ArrayList<String> wordArrayList, boolean type) {
		ArrayList<String> dateWords = getFirstTwoWords(wordArrayList);
		ArrayList<String> timeWords = getThirdAndFourthWords(wordArrayList);

		Date date = processAsTwoWordDate(dateWords, type);
		Date time = processAsTwoWordTime(timeWords);

		if (date != NO_RESULT && time != NO_RESULT) {
			return combineDateAndTime(date, time);
		}
		return NO_RESULT;
	}

	private static ArrayList<String> getFirstTwoWords(
			ArrayList<String> wordArrayList) {
		assert (wordArrayList.size() >= LIST_SIZE_TWO_WORDS);
		// Sublist extract from first given index up to before the second given
		// index
		return new ArrayList<String>(wordArrayList.subList(
				ARRAY_INDEX_FIRST_WORD, ARRAY_INDEX_THIRD_WORD));

	}

	private static ArrayList<String> getSecondAndThirdWord(
			ArrayList<String> wordArrayList) {
		assert (wordArrayList.size() >= LIST_SIZE_THREE_WORDS);
		// Sublist extract from first given index up to before the second given
		// index
		return new ArrayList<String>(wordArrayList.subList(
				ARRAY_INDEX_SECOND_WORD, ARRAY_INDEX_FOURTH_WORD));
	}

	private static ArrayList<String> getThirdAndFourthWords(
			ArrayList<String> wordArrayList) {
		assert (wordArrayList.size() >= LIST_SIZE_FOUR_WORDS);
		// Sublist extract from first given index up to before the second given
		// index
		return new ArrayList<String>(wordArrayList.subList(
				ARRAY_INDEX_THIRD_WORD, ARRAY_INDEX_FIFTH_WORD));

	}

	private static Date processAsDate(String string, boolean type) {
		if (isEmptyString(string)) {
			return NO_RESULT;
		}

		Date date = processAsFormat(string, FORMAT_DAY_MONTH_YEAR);

		if (date == NO_RESULT) {
			date = processAsDateWithoutYear(string);
			if (date == NO_RESULT) {
				date = processAsWord(string);
			}
		}

		if (date != NO_RESULT) {
			date = setDefaultTime(date, type);
		}

		return date;
	}

	private static Date processAsDateWithoutYear(String string) {
		Date date = NO_RESULT;
		date = processAsFormat(string, FORMAT_DAY_MONTH);
		if (date != NO_RESULT) {
			date = addYear(date);
		}

		return date;
	}

	// Takes the given date object and add this year to it
	private static Date addYear(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);

		int year = Calendar.getInstance().get(Calendar.YEAR);
		calendar.set(Calendar.YEAR, year);
		return calendar.getTime();
	}

	private static Date processAsWord(String string) {
		Date date = NO_RESULT;

		date = processAsToday(string);
		if (date != NO_RESULT) {
			return date;
		}

		date = processAsTomorrow(string);
		if (date != NO_RESULT) {
			return date;
		}

		date = processAsDayOfTheWeek(string);

		return date;
	}

	private static Date processAsToday(String string) {
		if (string.equalsIgnoreCase(WORD_TODAY)) {
			return new Date(); // this gives current date and time
		}
		return NO_RESULT;
	}

	private static Date processAsTomorrow(String string) {
		if (string.equalsIgnoreCase(WORD_TOMORROW)) {
			return getTomorrowDate();
		}
		return NO_RESULT;
	}

	private static Date getTomorrowDate() {
		// Gives current date and Time
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.DATE, INTEGER_ONE_DAY);
		return calendar.getTime();

	}

	private static Date processAsDayOfTheWeek(String string) {
		int dayOfWeekNum = getDayOfWeekNumber(string);
		if (dayOfWeekNum == INTEGER_NOT_DAY_OF_THE_WEEK) {
			return NO_RESULT;
		}

		int currDayNum = getDayOfWeekNumber(new SimpleDateFormat(
				FORMAT_DAY_OF_WEEK_SHORT).format(new Date()));
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.DATE, dayOfWeekNum - currDayNum);

		return calendar.getTime();
	}

	private static int getDayOfWeekNumber(String string) {
		if (isWordForMonday(string)) {
			return INTEGER_FOR_MON;
		}
		if (isWordForTuesday(string)) {
			return INTEGER_FOR_TUE;
		}
		if (isWordForWednesday(string)) {
			return INTEGER_FOR_WED;
		}
		if (isWordForThursday(string)) {
			return INTEGER_FOR_THU;
		}
		if (isWordForFriday(string)) {
			return INTEGER_FOR_FRI;
		}
		if (isWordForSaturday(string)) {
			return INTEGER_FOR_SAT;
		}
		if (isWordForSunday(string)) {
			return INTEGER_FOR_SUN;
		}

		return INTEGER_NOT_DAY_OF_THE_WEEK;
	}

	private static boolean isWordForMonday(String string) {
		return string.equalsIgnoreCase(WORD_MON)
				|| string.equalsIgnoreCase(WORD_MONDAY);
	}

	private static boolean isWordForTuesday(String string) {
		return string.equalsIgnoreCase(WORD_TUE)
				|| string.equalsIgnoreCase(WORD_TUESDAY);
	}

	private static boolean isWordForWednesday(String string) {
		return string.equalsIgnoreCase(WORD_WED)
				|| string.equalsIgnoreCase(WORD_WEDNESDAY);
	}

	private static boolean isWordForThursday(String string) {
		return string.equalsIgnoreCase(WORD_THU)
				|| string.equalsIgnoreCase(WORD_THURSDAY);
	}

	private static boolean isWordForFriday(String string) {
		return string.equalsIgnoreCase(WORD_FRI)
				|| string.equalsIgnoreCase(WORD_FRIDAY);
	}

	private static boolean isWordForSaturday(String string) {
		return string.equalsIgnoreCase(WORD_SAT)
				|| string.equalsIgnoreCase(WORD_SATURDAY);
	}

	private static boolean isWordForSunday(String string) {
		return string.equalsIgnoreCase(WORD_SUN)
				|| string.equalsIgnoreCase(WORD_SUNDAY);
	}

	private static Date processAsTime(String string) {
		if (isEmptyString(string)) {
			return NO_RESULT;
		}

		Date time = processAsFormat(string, FORMAT_12H_MIN_AMPM);
		if (time == NO_RESULT) {
			time = processAsFormat(string, FORMAT_12H_AMPM);
		}
		if (time == NO_RESULT) {
			time = processAsFormat(string, FORMAT_24H_MIN);
		}
		if (time != NO_RESULT) {
			time = setDayToToday(time);
		}
		return time;
	}

	private static Date setDayToToday(Date time) {
		return combineDateAndTime(new Date(), time);
	}

	private static Date combineDateAndTime(Date date, Date time) {
		String timeString = new SimpleDateFormat(FORMAT_24H_MIN).format(time);
		String dateString = new SimpleDateFormat(FORMAT_DAY_MONTH_YEAR)
				.format(date);
		return processAsFormat(dateString + " " + timeString,
				FORMAT_DAY_MONTH_YEAR + " " + FORMAT_24H_MIN);
	}

	private static Date setDefaultTime(Date date, boolean type) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		setDefaultTimes(date, type, calendar);
		return calendar.getTime();
	}

	private static void setDefaultTimes(Date date, boolean type,
			Calendar calendar) {
		calendar.set(Calendar.SECOND, DEFAULT_TIME_SECOND);
		calendar.set(Calendar.MILLISECOND, DEFAULT_TIME_MILISECOND);
		if (type == BOOLEAN_START_TIME) {
			setDefaultStartTime(calendar);
		} else if (type == BOOLEAN_END_TIME) {
			setDefaultEndTime(calendar);
		}

	}

	private static void setDefaultStartTime(Calendar calendar) {
		calendar.set(Calendar.HOUR_OF_DAY, DEFAULT_START_TIME_HOUR);
		calendar.set(Calendar.MINUTE, DEFAULT_START_TIME_MINUTE);
	}

	private static void setDefaultEndTime(Calendar calendar) {
		calendar.set(Calendar.HOUR_OF_DAY, DEFAULT_END_TIME_HOUR);
		calendar.set(Calendar.MINUTE, DEFAULT_END_TIME_MINUTE);
	}

	private static Date processAsTwoWordTime(ArrayList<String> wordArrayList) {
		if (wordArrayList.size() != LIST_SIZE_TWO_WORDS) {
			return NO_RESULT;
		}

		String timeString = wordArrayList.get(ARRAY_INDEX_FIRST_WORD)
				+ STRING_WHITESPACE
				+ wordArrayList.get(ARRAY_INDEX_SECOND_WORD);

		Date time = NO_RESULT;
		time = processAsFormat(timeString, FORMAT_12H_MIN_SPACE_AMPM);
		if (time == NO_RESULT) {
			time = processAsFormat(timeString, FORMAT_12H_SPACE_AMPM);
		}

		if (time != NO_RESULT) {
			time = setDayToToday(time);
		}

		return time;
	}

	private static Date processAsTwoWordDate(ArrayList<String> wordArrayList,
			boolean type) {
		if (wordArrayList.size() != LIST_SIZE_TWO_WORDS) {
			return NO_RESULT;
		}

		Date date = NO_RESULT;

		if (isFirstWordNext(wordArrayList)) {
			date = getDateInNextWeek(wordArrayList.get(1));
			if (date != NO_RESULT) {
				date = setDefaultTime(date, type);
			}
		}

		return date;
	}

	private static boolean isFirstWordNext(ArrayList<String> wordArrayList) {
		return wordArrayList.get(ARRAY_INDEX_FIRST_WORD).equalsIgnoreCase(
				WORD_NEXT)
				|| wordArrayList.get(ARRAY_INDEX_FIRST_WORD).equalsIgnoreCase(
						WORD_NXT);
	}

	private static Date getDateInNextWeek(String string) {

		Date day = processAsDayOfTheWeek(string);
		if (day != NO_RESULT) {
			day = addSevenDays(day);
		}
		return day;
	}

	private static Date addSevenDays(Date day) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(day);
		calendar.add(Calendar.DATE, INTEGER_DAYS_IN_A_WEEK);
		return calendar.getTime();
	}

	private static Date processAsFormat(String string, String format) {
		SimpleDateFormat dateFormat = new SimpleDateFormat(format);
		try {
			return dateFormat.parse(string);
		} catch (ParseException e) {
			return NO_RESULT;
		}

	}
}
